package cn.dansj.controller;

import cn.dansj.common.request.annotation.http.GetMethod;
import cn.dansj.common.request.annotation.http.RequestMappingMethod;
import cn.dansj.common.request.annotation.http.RequestParam;
import cn.dansj.common.utils.enums.BooleanType;
import cn.dansj.common.request.RequestUtils;
import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.visitor.SchemaStatVisitor;
import com.alibaba.druid.stat.DruidStatManagerFacade;
import com.alibaba.druid.stat.DruidStatServiceMBean;
import com.alibaba.druid.support.http.stat.WebAppStatManager;
import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import com.alibaba.druid.support.spring.stat.SpringStatManager;
import com.alibaba.druid.util.MapComparator;
import com.alibaba.druid.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 注意：避免直接调用Druid相关对象例如DruidDataSource等，相关调用要到DruidStatManagerFacade里用反射实现
 *
 * @author sandzhang[sandzhangtoo@gmail.com]
 */

@RequestMappingMethod(value = "/dataSource/druid", nologging = BooleanType.TRUE)
public class DruidMonitorController {
    private final DruidStatService statService = DruidStatService.getInstance();

    @GetMethod("/{*uri}")
    public String druidMonitor(@RequestParam(path = true) String uri, HttpServletRequest request) {
        final String service = statService.service(getFullUrl(uri, request.getQueryString()));
        if (DruidStatService.RESULT_CODE_NOT_FOUND.equals(service)) {
            return RequestUtils.response404();
        }
        return service;
    }

    private String getFullUrl(String uri, String params) {
        return uri + (params == null ? "" : "?" + params);
    }

    public static final class DruidStatService implements DruidStatServiceMBean {
        private static final Log LOG = LogFactory.getLog(DruidStatService.class);

        public static final String MBEAN_NAME = "com.alibaba.druid:type=DruidStatService";

        private static final DruidStatService instance = new DruidStatService();

        private static final DruidStatManagerFacade statManagerFacade = DruidStatManagerFacade.getInstance();

        public static final int RESULT_CODE_SUCCESS = 1;
        public static final int RESULT_CODE_ERROR = -1;
        public static final String RESULT_CODE_NOT_FOUND = "0";

        private static final int DEFAULT_PAGE = 1;
        private static final int DEFAULT_PER_PAGE_COUNT = Integer.MAX_VALUE;
        private static final String ORDER_TYPE_DESC = "desc";
        private static final String ORDER_TYPE_ASC = "asc";
        private static final String DEFAULT_ORDER_TYPE = ORDER_TYPE_ASC;
        private static final String DEFAULT_ORDER_BY = "SQL";

        private DruidStatService() {
        }

        public static DruidStatService getInstance() {
            return instance;
        }

        public boolean isResetEnable() {
            return statManagerFacade.isResetEnable();
        }

        public void setResetEnable(boolean value) {
            statManagerFacade.setResetEnable(value);
        }

        public String service(String url) {
            Map<String, String> parameters = getParameters(url);

            if (url.equals("/basic.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, statManagerFacade.returnJSONBasicStat());
            }

            if (url.equals("/reset-all.json")) {
                statManagerFacade.resetAll();
                return returnJSONResult(RESULT_CODE_SUCCESS, null);
            }

            if (url.equals("/log-and-reset.json")) {
                statManagerFacade.logAndResetDataSource();
                return returnJSONResult(RESULT_CODE_SUCCESS, null);
            }

            if (url.equals("/datasource.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, statManagerFacade.getDataSourceStatDataList());
            }

            if (url.equals("/activeConnectionStackTrace.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, statManagerFacade.getActiveConnStackTraceList());
            }

            if (url.startsWith("/datasource-")) {
                Object result = statManagerFacade.getDataSourceStatData(StringUtils.subStringToInteger(url, "datasource-", "."));
                return returnJSONResult(result == null ? RESULT_CODE_ERROR : RESULT_CODE_SUCCESS, result);
            }

            if (url.startsWith("/connectionInfo-") && url.endsWith(".json")) {
                List<?> connectionInfoList = statManagerFacade.getPoolingConnectionInfoByDataSourceId(StringUtils.subStringToInteger(url, "connectionInfo-", "."));
                return returnJSONResult(connectionInfoList == null ? RESULT_CODE_ERROR : RESULT_CODE_SUCCESS, connectionInfoList);
            }

            if (url.startsWith("/activeConnectionStackTrace-") && url.endsWith(".json")) {
                return returnJSONActiveConnectionStackTrace(StringUtils.subStringToInteger(url, "activeConnectionStackTrace-", "."));
            }

            if (url.startsWith("/sql.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getSqlStatDataList(parameters));
            }

            if (url.startsWith("/wall.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getWallStatMap(parameters));
            }

            if (url.startsWith("/wall-") && url.indexOf(".json") > 0) {
                Object result = statManagerFacade.getWallStatMap(StringUtils.subStringToInteger(url, "wall-", ".json"));
                return returnJSONResult(result == null ? RESULT_CODE_ERROR : RESULT_CODE_SUCCESS, result);
            }

            if (url.startsWith("/sql-") && url.indexOf(".json") > 0) {
                return getSqlStat(StringUtils.subStringToInteger(url, "sql-", ".json"));
            }

            if (url.startsWith("/weburi.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getWebURIStatDataList(parameters));
            }

            if (url.startsWith("/weburi-") && url.indexOf(".json") > 0) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getWebURIStatData(StringUtils.subString(url, "weburi-", ".json", true)));
            }

            if (url.startsWith("/webapp.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getWebAppStatDataList(parameters));
            }

            if (url.startsWith("/websession.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getWebSessionStatDataList(parameters));
            }

            if (url.startsWith("/websession-") && url.indexOf(".json") > 0) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getWebSessionStatData(StringUtils.subString(url, "websession-", ".json")));
            }

            if (url.startsWith("/spring.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getSpringStatDataList(parameters));
            }

            if (url.startsWith("/spring-detail.json")) {
                return returnJSONResult(RESULT_CODE_SUCCESS, getSpringMethodStatData(parameters.get("class"), parameters.get("method")));
            }

            return RESULT_CODE_NOT_FOUND;
        }

        private List<Map<String, Object>> getSpringStatDataList(Map<String, String> parameters) {
            return comparatorOrderBy(SpringStatManager.getInstance().getMethodStatData(), parameters);
        }

        private List<Map<String, Object>> getWebURIStatDataList(Map<String, String> parameters) {
            return comparatorOrderBy(WebAppStatManager.getInstance().getURIStatData(), parameters);
        }

        private Map<String, Object> getWebURIStatData(String uri) {
            return WebAppStatManager.getInstance().getURIStatData(uri);
        }

        private Map<String, Object> getWebSessionStatData(String sessionId) {
            return WebAppStatManager.getInstance().getSessionStat(sessionId);
        }

        private Map<String, Object> getSpringMethodStatData(String clazz, String method) {
            return SpringStatManager.getInstance().getMethodStatData(clazz, method);
        }

        private List<Map<String, Object>> getWebSessionStatDataList(Map<String, String> parameters) {
            return comparatorOrderBy(WebAppStatManager.getInstance().getSessionStatData(), parameters);
        }

        private List<Map<String, Object>> getWebAppStatDataList(Map<String, String> parameters) {
            return comparatorOrderBy(WebAppStatManager.getInstance().getWebAppStatData(), parameters);
        }

        private List<Map<String, Object>> comparatorOrderBy(List<Map<String, Object>> array, Map<String, String> parameters) {
            // when open the stat page before executing some sql
            if (array == null || array.isEmpty()) {
                return null;
            }

            // when parameters is null
            String orderBy, orderType;
            int page = DEFAULT_PAGE;
            int perPageCount = DEFAULT_PER_PAGE_COUNT;
            if (parameters == null) {
                orderBy = DEFAULT_ORDER_BY;
                orderType = DEFAULT_ORDER_TYPE;
            } else {
                orderBy = parameters.get("orderBy");
                orderType = parameters.get("orderType");
                String pageParam = parameters.get("page");
                if (pageParam != null && pageParam.length() != 0) {
                    page = Integer.parseInt(pageParam);
                }
                String pageCountParam = parameters.get("perPageCount");
                if (pageCountParam != null && pageCountParam.length() > 0) {
                    perPageCount = Integer.parseInt(pageCountParam);
                }
            }

            // others,such as order
            orderBy = orderBy == null ? DEFAULT_ORDER_BY : orderBy;
            orderType = orderType == null ? DEFAULT_ORDER_TYPE : orderType;

            if (!ORDER_TYPE_DESC.equals(orderType)) {
                orderType = ORDER_TYPE_ASC;
            }

            // orderBy the statData array
            if (orderBy.trim().length() != 0) {
                array.sort(new MapComparator<>(orderBy, ORDER_TYPE_DESC.equals(orderType)));
            }

            // page
            int fromIndex = (page - 1) * perPageCount;
            int toIndex = page * perPageCount;
            if (toIndex > array.size()) {
                toIndex = array.size();
            }

            return array.subList(fromIndex, toIndex);
        }

        private List<Map<String, Object>> getSqlStatDataList(Map<String, String> parameters) {
            Integer dataSourceId = null;

            String dataSourceIdParam = parameters.get("dataSourceId");
            if (dataSourceIdParam != null && dataSourceIdParam.length() > 0) {
                dataSourceId = Integer.parseInt(dataSourceIdParam);
            }

            return comparatorOrderBy(statManagerFacade.getSqlStatDataList(dataSourceId), parameters);
        }

        @SuppressWarnings("unchecked")
        public Map<String, Object> getWallStatMap(Map<String, String> parameters) {
            Integer dataSourceId = null;

            String dataSourceIdParam = parameters.get("dataSourceId");
            if (dataSourceIdParam != null && dataSourceIdParam.length() > 0) {
                dataSourceId = Integer.parseInt(dataSourceIdParam);
            }

            Map<String, Object> result = statManagerFacade.getWallStatMap(dataSourceId);

            if (result != null) {
                List<Map<String, Object>> tables = (List<Map<String, Object>>) result.get("tables");
                if (tables != null) {
                    result.put("tables", comparatorOrderBy(tables, parameters));
                }

                List<Map<String, Object>> functions = (List<Map<String, Object>>) result.get("functions");
                if (functions != null) {
                    result.put("functions", comparatorOrderBy(functions, parameters));
                }
            } else {
                result = Collections.emptyMap();
            }

            return result;
        }

        private String getSqlStat(Integer id) {
            Map<String, Object> map = statManagerFacade.getSqlStatData(id);

            if (map == null) {
                return returnJSONResult(RESULT_CODE_ERROR, null);
            }

            DbType dbType = DbType.of((String) map.get("DbType"));
            String sql = (String) map.get("SQL");

            map.put("formattedSql", SQLUtils.format(sql, dbType));
            List<SQLStatement> statementList = SQLUtils.parseStatements(sql, dbType);

            if (!statementList.isEmpty()) {
                SQLStatement sqlStmt = statementList.get(0);
                SchemaStatVisitor visitor = SQLUtils.createSchemaStatVisitor(dbType);
                sqlStmt.accept(visitor);
                map.put("parsedTable", visitor.getTables().toString());
                map.put("parsedFields", visitor.getColumns().toString());
                map.put("parsedConditions", visitor.getConditions().toString());
                map.put("parsedRelationships", visitor.getRelationships().toString());
                map.put("parsedOrderbycolumns", visitor.getOrderByColumns().toString());
            }

            DateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss:SSS");
            Date maxTimespanOccurTime = (Date) map.get("MaxTimespanOccurTime");
            if (maxTimespanOccurTime != null) {
                map.put("MaxTimespanOccurTime", format.format(maxTimespanOccurTime));
            }

            return returnJSONResult(RESULT_CODE_SUCCESS, map);
        }

        private String returnJSONActiveConnectionStackTrace(Integer id) {
            List<String> result = statManagerFacade.getActiveConnectionStackTraceByDataSourceId(id);

            if (result == null) {
                return returnJSONResult(RESULT_CODE_ERROR, "require set removeAbandoned=true");
            }
            return returnJSONResult(RESULT_CODE_SUCCESS, result);
        }

        public static String returnJSONResult(int resultCode, Object content) {
            Map<String, Object> dataMap = new LinkedHashMap<>();
            dataMap.put("ResultCode", resultCode);
            dataMap.put("Content", content);
            return JSONUtils.toJSONString(dataMap);
        }

        public static void registerMBean() {
            MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
            try {
                ObjectName objectName = new ObjectName(MBEAN_NAME);
                if (!mbeanServer.isRegistered(objectName)) {
                    mbeanServer.registerMBean(instance, objectName);
                }
            } catch (JMException ex) {
                LOG.error("register mbean error", ex);
            }
        }

        public static void unregisterMBean() {
            MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();

            try {
                mbeanServer.unregisterMBean(new ObjectName(MBEAN_NAME));
            } catch (JMException ex) {
                LOG.error("unregister mbean error", ex);
            }
        }

        public static Map<String, String> getParameters(String url) {
            if (url == null || (url = url.trim()).length() == 0) {
                return Collections.emptyMap();
            }

            String parametersStr = StringUtils.subString(url, "?", null);
            if (parametersStr == null || parametersStr.length() == 0) {
                return Collections.emptyMap();
            }

            String[] parametersArray = parametersStr.split("&");
            Map<String, String> parameters = new LinkedHashMap<>();

            for (String parameterStr : parametersArray) {
                int index = parameterStr.indexOf("=");
                if (index <= 0) {
                    continue;
                }

                String name = parameterStr.substring(0, index);
                String value = parameterStr.substring(index + 1);
                parameters.put(name, value);
            }
            return parameters;
        }
    }
}
